C/C++实现高性能并行计算 您所在的位置:网站首页 高性能计算 gpu 知识点 C/C++实现高性能并行计算

C/C++实现高性能并行计算

2024-07-05 23:09| 来源: 网络整理| 查看: 265

系列文章目录 pthreads并行编程(上)pthreads并行编程(中)pthreads并行编程(下)使用OpenMP进行共享内存编程

文章目录 系列文章目录前言一、简单介绍OpenMP二、预处理器指令三、Hello world代码四、`pragma omp parallel for num_thread`五、数据依赖六、OpenMP Private Variables6.1 firstprivate6.2 lastprivate 七、section & sections7.1 single & master指令 八、reduction(归并)九、同步与互斥9.1 barrier9.1.1 nowait 9.2 竞争9.2.1 critical9.2.2 atomic 十、schedule(调度)10.1 schedule指令的类型10.2 静态调度10.3 动态调度10.4 指导性调度 总结参考

前言

OpenMP(Open Multi-Processing)是一个支持多平台共享内存多处理编程的应用程序接口(API),它用于编写在多处理器计算机上高效运行的程序。OpenMP是一种使用编译器指令以及库调用和环境变量来实现的并行编程模型。它主要用于C、C++和Fortran语言。

OpenMP和Pthread都是统一内存访问,而MPI是非统一内存访问。

一、简单介绍OpenMP

在这里插入图片描述 OpenMP在编译过程中通过代码中的简单声明,编译器会自动进行并行(这就需要编译器支持一些操作)

在这里插入图片描述 在这里插入图片描述

二、预处理器指令

预处理器指令是C、C++和其他一些编程语言中的一种特殊的代码行,它们在编译代码之前由预处理器执行。预处理器指令提供了一种机制,通过它可以在实际编译之前对源代码文件进行修改和配置。这些指令以井号(#)开始,表明它们是给预处理器的指示,而不是编译器。

OpenMP提供基于指令的共享内存API:OpenMP通过编译器指令(也称为编译器指示或编译器pragma)来实现共享内存并行编程模型。这种方式允许程序员通过在代码中插入特定的指令来控制多线程的行为,而无需修改底层线程管理或内存同步的机制。这些指令为程序员提供了一个相对简单的方法来实现并行处理,同时还可以保持代码的可读性和可维护性。

#pragma:这条指令用于向编译器提供特定的命令或指示,其具体含义依赖于编译器。例如,在OpenMP中,#pragma omp parallel用于指示编译器下面的代码块应该并行执行。

基于指令的共享内存API的工作原理:

共享内存并行性:在OpenMP中,所有线程都可以访问相同的物理内存地址空间(共享内存),这意味着任何线程都可以访问程序中的任何变量。这种模型简化了数据的管理和访问,但也需要注意数据同步和防止竞争条件。编译器指令:OpenMP使用特殊的预处理指令(如#pragma omp)来标识并行区域的开始和结束,以及其他并行控制结构,例如循环并行化(#pragma omp for)、任务并行化(#pragma omp task)等。这些指令告诉编译器哪些代码块应该并行执行。简化并行编程:通过使用这些指令,OpenMP允许开发者专注于程序的逻辑部分,而不必担心底层的线程创建、管理或复杂的同步机制。编译器和OpenMP运行时库共同处理这些复杂的任务。

为了使用gcc编译omp程序,需要包含-fopenmp选项

三、Hello world代码 #include #include #include void hello(void){ | int my_rank = omp_get_thread_num(); | int thread_count = omp_get_num_threads(); // 一共有多少个线程 | printf("Hello from thread %d of %d.\n", my_rank, thread_count); | return; } int main(int argc, char* argv[]){ | int thread_count = strtol(argv[1], NULL, 10); #pragma omp parallel num_threads(thread_count) | hello(); | return 0; }

运行结果: 在这里插入图片描述在这里插入图片描述

注意:如果没有指定 num_threads 子句,OpenMP会自动根据环境设置(如环境变量或系统配置)决定使用多少线程。这可以是一个基于系统资源自动优化的选择。

四、pragma omp parallel for num_thread

在这里插入图片描述如果只想要for循环并行的话,上述的parallel是必须要的

例子:

#pragma omp parallel for num_threads(thread_count) for (int i = 0; i tmp = a[i]/b[i]; c[i] = tmp*tmp; }

在这里插入图片描述

6.1 firstprivate

在OpenMP中,firstprivate是一种数据共享属性指令,它用来管理变量在并行区域中的作用域和行为。使用firstprivate指令时,每个线程获得指定变量的私有拷贝,且该拷贝的初始值是该变量在并行区域开始前的值。

firstprivate 的功能和用途:

初始值的复制:通常在并行区域内创建的私有变量不会自动初始化,即它们的初始值是不确定的。但当使用firstprivate时,每个线程中的私有变量都会以并行区域之外该变量的最后值作为初始值。

适用性:这个属性特别有用当需要在并行执行的多个线程中使用同一变量,但又希望每个线程都有自己的一份带有初始值的拷贝时。

避免数据竞争:通过为每个线程创建变量的私有拷贝,firstprivate有助于避免数据竞争和不必要的数据依赖,这对于保持代码的正确性和提高性能非常重要。

下面是一个简单的例子,展示了如何在OpenMP中使用firstprivate:

#include #include int main() { int a = 5; // 初始化变量a #pragma omp parallel for firstprivate(a) for (int i = 0; i int x; #pragma omp parallel for lastprivate(x) for (int i = 0; i #pragma omp parallel sections { #pragma omp section { printf("Section 1 executed by thread %d\n", omp_get_thread_num()); // 在这里执行Section 1的任务 } #pragma omp section { printf("Section 2 executed by thread %d\n", omp_get_thread_num()); // 在这里执行Section 2的任务 } #pragma omp section { printf("Section 3 executed by thread %d\n", omp_get_thread_num()); // 在这里执行Section 3的任务 } } return 0; } sections在封闭代码的指定部分中,由线程进行分配任务每个独立的section都需要在sections里面 每个section都是被一个线程执行的不同的section可能执行不同的任务如果一个线程够快,该线程可能执行多个section

注意事项:

确保每个 section 中的代码可以安全地并行执行,不会出现数据竞争或其它并发问题。如果 sections 数量超过了线程数,有些线程可能会执行多个 section,或者某些 section 可能会排队等待可用线程。

通过合理使用 section 指令,可以在OpenMP程序中实现复杂的并行任务,从而有效提升程序性能和并行处理能力。

7.1 single & master指令

在这里插入图片描述在这里插入图片描述 为什么最后是约等于呢?是因为最后那个执行这段命令的不一定是rank等于0的线程,有可能是其他线程。

八、reduction(归并)

reduction 是一种非常有用的数据共享属性指令,用于在并行计算中实现对特定变量进行跨多个线程的归约操作。这种归约操作包括求和、求积、找最大值、最小值等,是并行计算中常见的需求。

在这里插入图片描述在这里插入图片描述本地变量的初始化的值相当于单位元

#include #include int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int sum = 10; #pragma omp parallel for reduction(+:sum) for (int i = 0; i int a[10]; int total = 0; #pragma omp parallel num_threads(4) { int tid = omp_get_thread_num(); // 每个线程初始化数组的一部分 #pragma omp for for (int i = 0; i total += a[i]; } } printf("Total sum is %d\n", total); return 0; }

在这个程序中:

数组a被所有线程共同初始化。每个线程负责数组的一部分。使用#pragma omp barrier确保所有线程都完成了数组的初始化工作。这是必要的,因为数组元素的初始化和累加是分开进行的,我们需要确保在开始累加之前所有元素都已经被正确初始化。在barrier之后,所有线程共同参与对数组元素的累加操作,使用了reduction来合并结果。

注意事项:

barrier 会引入显著的同步开销,因为它需要等待所有线程到达同步点。因此,应当仅在必要时使用。OpenMP的循环指令(如#pragma omp for)在循环结束时自带隐式的barrier,除非指定了nowait子句。

使用barrier可以有效地控制程序的执行流程,在需要严格同步的场景中保证数据的一致性和正确性。

在这里插入图片描述

9.1.1 nowait

nowait在OpenMP中,用于打断自动添加的barrier的类型。用法如下

#pragma omp for nowait #pragma omp signle nowait

在这里插入图片描述

9.2 竞争

以下是正确的程序运行的时候 在这里插入图片描述 下面是一种错误(出现竞争)的情况 在这里插入图片描述 链表节点并行插入也会有竞争情况 在这里插入图片描述

9.2.1 critical

在这里插入图片描述一段代码只能由一个线程进行,这段代码就是临界区,往往和共享数据更新有关

添加头节点示例:

void addHead(struct List* list, struct Node* node){ #pragma omp critical { node->next = list->head; list->head = node; } } 9.2.2 atomic

在编程中,“原子执行”(Atomic Execution)是指在多线程环境下,一个操作或一系列操作作为一个不可分割的单元执行,即这个操作要么完全执行,要么完全不执行,不会被其他线程的操作打断。这是并行计算中确保数据一致性和防止竞态条件的关键概念。

原子操作的特点

不可中断:原子操作一旦开始,就会运行完成,不会在中间被其他线程的操作中断。可见性:当原子操作完成时,其结果立即对所有线程可见。序列化:对同一数据的原子操作,从任何线程看来都是按一定顺序执行的,这个顺序由系统的调度决定。

它只在特殊的情况下使用:

在自增或者自减的情况下使用在二元操作数的情况下使用只会应用于一条指令: #pragma omp atomic counter += 5;

下面是一个使用OpenMP的atomic指令的示例,展示如何安全地在多线程环境中更新共享变量:

#include #include int main() { int sum = 0; #pragma omp parallel num_threads(4) { #pragma omp for for (int i = 0; i int i; // 使用默认块大小的静态调度 #pragma omp parallel for schedule(static) for (i = 0; i printf("Thread %d handles iteration %d (static with chunk size 3)\n", omp_get_thread_num(), i); } return 0; }

优点:

低调度开销:由于迭代在运行之前就已经分配好,不需要在运行时进行额外的调度计算,这减少了调度开销。预测性好:迭代的分配是可预测的,这有助于理解程序的行为并优化性能。

缺点:

负载不平衡:如果循环迭代之间的执行时间差异较大,静态调度可能会导致严重的负载不平衡,使得某些线程早早完成任务而其他线程还在忙碌,从而影响程序的总体性能。

在这里插入图片描述

10.3 动态调度

动态调度(dynamic scheduling)是一种在运行时动态分配循环迭代给线程的方法。这种调度方式适用于循环迭代之间的工作负载可能存在较大差异的情况,因为它能够在运行时根据各线程的工作进度动态调整迭代的分配,从而帮助实现更好的负载平衡。

工作原理: 动态调度将循环迭代分组到一定大小的“块”中,并在运行时将这些块分配给请求任务的线程。当一个线程完成其当前块的所有迭代后,它会再次向调度器请求新的迭代块,直到所有的迭代都被处理完毕。

块大小(Chunk Size)

块大小是指定给每个线程的连续迭代数。块大小的选择对性能有重要影响: 较小的块大小可以增强负载平衡,因为工作更频繁地在线程之间重新分配。较大的块大小可以减少调度开销,因为线程不需要那么频繁地从调度器请求新的工作。 #include #include #include // For sleep function int main() { // 使用块大小为2的动态调度 #pragma omp parallel for schedule(dynamic, 2) for (int i = 0; i // 使用指导性调度,指定最小块大小为1 #pragma omp parallel for schedule(guided, 1) for (int i = 0; i


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有